﻿
namespace Apache.Cassandra.Client {
    using System;
    using System.ComponentModel;
    using System.Net.NetworkInformation;
    using System.Runtime.InteropServices;
    using System.Security.Cryptography;
    using System.Threading;

    [StructLayout(LayoutKind.Sequential)]
    [Serializable]
    [TypeConverter(typeof(UuidConverter))]
    public struct Uuid : IFormattable, IComparable {

        public static readonly Uuid Empty = new Uuid(); 

        private static RNGCryptoServiceProvider rng;
        private static RNGCryptoServiceProvider Rng {
            get {
                if (null == rng) {
                    RNGCryptoServiceProvider value = new RNGCryptoServiceProvider();
                    if (null != Interlocked.CompareExchange(ref rng, value, null)) {
                        value.Dispose();
                    }
                }

                return rng;
            }
        }

        private int _time_low;
        private short _time_mid;
        private short _time_hi_and_version;
        private byte _clk_seq_hi_res;
        private byte _clk_seq_low;
        private byte _nodea;
        private byte _nodeb;
        private byte _nodec;
        private byte _noded;
        private byte _nodee;
        private byte _nodef;

        public Uuid(byte[] b) {
            if (null == b) {
                throw new ArgumentNullException("b");
            }
            if (16 != b.Length) {
                throw new ArgumentException("b");
            }

            _time_low = ((int)b[0] << 24) | ((int)b[1] << 16) | ((int)b[2] << 8) | b[3];
            _time_mid = (short)(((int)b[4] << 8) | b[5]);
            _time_hi_and_version = (short)(((int)b[6] << 8) | b[7]);
            _clk_seq_hi_res = b[8];
            _clk_seq_low = b[9];
            _nodea = b[10];
            _nodeb = b[11];
            _nodec = b[12];
            _noded = b[13];
            _nodee = b[14];
            _nodef = b[15];
        }

        public Uuid(int time_low, short time_mid, short time_hi_and_version, byte clk_seq_hi_res, byte clk_seq_low, byte[] node) {
            if (node == null) {
                throw new ArgumentNullException("node");
            }
            if (node.Length != 6) {
                throw new ArgumentException("node");
            }

            _time_low = time_low;
            _time_mid = time_mid;
            _time_hi_and_version = time_hi_and_version;
            _clk_seq_hi_res = clk_seq_hi_res;
            _clk_seq_low = clk_seq_low;
            _nodea = node[0];
            _nodeb = node[1];
            _nodec = node[2];
            _noded = node[3];
            _nodee = node[4];
            _nodef = node[5];
        }

        public Uuid(string guid) {
            if (null == guid) {
                throw new ArgumentNullException("guid");
            }

            string[] parts = guid.Split('-');
            if (5 != parts.Length) {
                throw new FormatException();
            }

            _time_low = Convert.ToInt32(parts[0], 16);
            _time_mid = Convert.ToInt16(parts[1], 16);
            _time_hi_and_version = Convert.ToInt16(parts[2], 16);

            short clk_seq = Convert.ToInt16(parts[3], 16);
            _clk_seq_hi_res = (byte)(clk_seq >> 8);
            _clk_seq_low = (byte)(clk_seq);

            long node = Convert.ToInt64(parts[4], 16);
            _nodea = (byte)(node >> 40);
            _nodeb = (byte)(node >> 32);
            _nodec = (byte)(node >> 24);
            _noded = (byte)(node >> 16);
            _nodee = (byte)(node >> 8);
            _nodef = (byte)(node);
        }

        public byte[] ToByteArray() {
            byte[] u = new byte[16];

            // time_low
            u[0] = (byte)(_time_low >> 24);
            u[1] = (byte)(_time_low >> 16);
            u[2] = (byte)(_time_low >> 8);
            u[3] = (byte)(_time_low);
            // time_mid
            u[4] = (byte)(_time_mid >> 8);
            u[5] = (byte)(_time_mid);
            // time_hi_and_version
            u[6] = (byte)(_time_hi_and_version >> 8);
            u[7] = (byte)(_time_hi_and_version);
            // clk_seq_hi_res
            u[8] = _clk_seq_hi_res;
            // clk_seq_low
            u[9] = _clk_seq_low;
            // node
            u[10] = _nodea;
            u[11] = _nodeb;
            u[12] = _nodec;
            u[13] = _noded;
            u[14] = _nodee;
            u[15] = _nodef;

            return u;
        }

        public override String ToString() { 
            return ToString("D", null);
        } 

        public override int GetHashCode() { 
            return _time_low ^ (((int)_time_mid << 16) | (int)(ushort)_time_hi_and_version) ^ (((int)_nodea << 24) | _nodef);
        }

        public override bool Equals(Object o) {
            if (o == null || !(o is Uuid)) {
                return false;
            }

            Uuid u = (Uuid)o;

            if (u._time_low != _time_low) {
                return false;
            }
            if (u._time_mid != _time_mid) {
                return false;
            }
            if (u._time_hi_and_version != _time_hi_and_version) {
                return false;
            }
            if (u._clk_seq_hi_res != _clk_seq_hi_res) {
                return false;
            }
            if (u._clk_seq_low != _clk_seq_low) {
                return false;
            }
            if (u._nodea != _nodea) {
                return false;
            }
            if (u._nodeb != _nodeb) {
                return false;
            }
            if (u._nodec != _nodec) {
                return false;
            }
            if (u._noded != _noded) {
                return false;
            }
            if (u._nodee != _nodee) {
                return false;
            }
            if (u._nodef != _nodef) {
                return false;
            }

            return true;
        }

        public bool Equals(Uuid u) {
            if (u._time_low != _time_low) {
                return false;
            }
            if (u._time_mid != _time_mid) {
                return false;
            }
            if (u._time_hi_and_version != _time_hi_and_version) {
                return false;
            }
            if (u._clk_seq_hi_res != _clk_seq_hi_res) {
                return false;
            }
            if (u._clk_seq_low != _clk_seq_low) {
                return false;
            }
            if (u._nodea != _nodea) {
                return false;
            }
            if (u._nodeb != _nodeb) {
                return false;
            }
            if (u._nodec != _nodec) {
                return false;
            }
            if (u._noded != _noded) {
                return false;
            }
            if (u._nodee != _nodee) {
                return false;
            }
            if (u._nodef != _nodef) {
                return false;
            }

            return true;
        }

        private int GetResult(uint me, uint them) {
            return me < them ? -1 : 1;
        }
 
        public int CompareTo(Object value) {
            if (value == null) {
                return 1;
            }
            if (!(value is Uuid)) {
                throw new ArgumentException();
            }

            Uuid u = (Uuid)value;
 
            if (u._time_low != this._time_low) {
                return GetResult((uint)this._time_low, (uint)u._time_low);
            }
            if (u._time_mid != this._time_mid) {
                return GetResult((uint)this._time_mid, (uint)u._time_mid);
            }
            if (u._time_hi_and_version != this._time_hi_and_version) {
                return GetResult((uint)this._time_hi_and_version, (uint)u._time_hi_and_version);
            }
            if (u._clk_seq_hi_res != this._clk_seq_hi_res) {
                return GetResult((uint)this._clk_seq_hi_res, (uint)u._clk_seq_hi_res);
            }
            if (u._clk_seq_low != this._clk_seq_low) {
                return GetResult((uint)this._clk_seq_low, (uint)u._clk_seq_low);
            }
            if (u._nodea != this._nodea) {
                return GetResult((uint)this._nodea, (uint)u._nodea);
            }
            if (u._nodeb != this._nodeb) {
                return GetResult((uint)this._nodeb, (uint)u._nodeb);
            }
            if (u._nodec != this._nodec) {
                return GetResult((uint)this._nodec, (uint)u._nodec);
            }
            if (u._noded != this._noded) {
                return GetResult((uint)this._noded, (uint)u._noded);
            }
            if (u._nodee != this._nodee) {
                return GetResult((uint)this._nodee, (uint)u._nodee);
            }
            if (u._nodef != this._nodef) {
                return GetResult((uint)this._nodef, (uint)u._nodef);
            }

            return 0; 
        }

        public static bool operator ==(Uuid a, Uuid b) {
            if (a._time_low != b._time_low) {
                return false;
            }
            if (a._time_mid != b._time_mid) {
                return false;
            }
            if (a._time_hi_and_version != b._time_hi_and_version) {
                return false;
            }
            if (a._clk_seq_hi_res != b._clk_seq_hi_res) {
                return false;
            }
            if (a._clk_seq_low != b._clk_seq_low) {
                return false;
            }
            if (a._nodea != b._nodea) {
                return false;
            }
            if (a._nodeb != b._nodeb) {
                return false;
            }
            if (a._nodec != b._nodec) {
                return false;
            }
            if (a._noded != b._noded) {
                return false;
            }
            if (a._nodee != b._nodee) {
                return false;
            }
            if (a._nodef != b._nodef) {
                return false;
            }

            return true;
        }

        public static bool operator !=(Uuid a, Uuid b) {
            return !(a == b);
        }

        private static readonly long Epoch = new DateTime(1582, 10, 15).Ticks;
        private static readonly byte[] Node = GetNodeId();

        public static Uuid NewTimeUuid() {
            return NewTimeUuid(DateTime.UtcNow.Ticks - Epoch);
        }

        public static Uuid NewTimeUuid(long ticks) {
            if (ticks < 0) {
                throw new ArgumentException("ticks");
            }

            // Clock sequence
            byte[] sequence = new byte[2];
            // Node OUI
            byte[] oui = new byte[3];

            Rng.GetBytes(sequence);
            Rng.GetBytes(oui);

            // 60-bit timestamp
            int time_low = (int)(ticks);
            short time_mid = (short)(ticks >> 32);
            short time_hi_and_version = (short)(((ticks >> 48) & 0x0FFF) | 0x1000);

            // 14-bit clock sequence
            byte clk_seq_hi_res = (byte)((sequence[0] & 0x3F) | 0x80);
            byte clk_seq_low = sequence[1];

            // Node
            byte[] node = new byte[6];
            node[0] = (byte)(oui[0] | 0x03);
            node[1] = oui[1];
            node[2] = oui[2];
            node[3] = Uuid.Node[3];
            node[4] = Uuid.Node[4];
            node[5] = Uuid.Node[5];

            return new Uuid(time_low, time_mid, time_hi_and_version, clk_seq_hi_res, clk_seq_low, node);
        }

        public static Uuid NewRandomUuid() {
            byte[] bytes = new byte[16];
            Rng.GetBytes(bytes);

            // Version
            bytes[6] &= 0x0f;
            bytes[6] |= 0x40;

            // Variant
            bytes[8] &= 0x3f;
            bytes[8] |= 0x80;

            return new Uuid(bytes);
        }

        public String ToString(String format) {
            return ToString(format, null);
        }
 
        private static char HexToChar(int a) {
            a = a & 0xf;
            return (char) ((a > 9) ? a - 10 + 0x61 : a + 0x30);
        }

        private static int HexsToChars(char[] guidChars, int offset, int a, int b) {
            guidChars[offset++] = HexToChar(a>>4);
            guidChars[offset++] = HexToChar(a);
            guidChars[offset++] = HexToChar(b>>4);
            guidChars[offset++] = HexToChar(b);
            return offset;
        }

        public String ToString(String format, IFormatProvider provider) {
            if (format == null || format.Length == 0) {
                format = "D";
            }

            char[] guidChars;
            int offset = 0;
            int strLength = 38;
            bool dash = true;
 
            if( format.Length != 1) {
                // all acceptable format string are of length 1
                throw new FormatException(); 
            }

            char formatCh = format[0];
            if (formatCh == 'D' || formatCh == 'd') {
                guidChars = new char[36];
                strLength = 36;
            } else if (formatCh == 'N' || formatCh == 'n') {
                guidChars = new char[32];
                strLength = 32;
                dash = false;
            } else if (formatCh == 'B' || formatCh == 'b') {
                guidChars = new char[38];
                guidChars[offset++] = '{';
                guidChars[37] = '}';
            } else if (formatCh == 'P' || formatCh == 'p') {
                guidChars = new char[38];
                guidChars[offset++] = '(';
                guidChars[37] = ')';
            } else {
                throw new FormatException();
            }

            offset = HexsToChars(guidChars, offset, _time_low >> 24, _time_low >> 16);
            offset = HexsToChars(guidChars, offset, _time_low >> 8, _time_low);

            if (dash) guidChars[offset++] = '-';
 
            offset = HexsToChars(guidChars, offset, _time_mid >> 8, _time_mid);
 
            if (dash) guidChars[offset++] = '-';

            offset = HexsToChars(guidChars, offset, _time_hi_and_version >> 8, _time_hi_and_version);

            if (dash) guidChars[offset++] = '-';

            offset = HexsToChars(guidChars, offset, _clk_seq_hi_res, _clk_seq_low);

            if (dash) guidChars[offset++] = '-';
 
            offset = HexsToChars(guidChars, offset, _nodea, _nodeb);
            offset = HexsToChars(guidChars, offset, _nodec, _noded);
            offset = HexsToChars(guidChars, offset, _nodee, _nodef);

            return new String(guidChars, 0, strLength);
        } 

        private static byte[] GetNodeId() {
            NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces();

            if (nics != null && nics.Length > 0) {
                PhysicalAddress address = nics[0].GetPhysicalAddress();

                return address.GetAddressBytes();
            } else {
                byte[] address = new byte[6];
                Rng.GetBytes(address);
                return address;
            }
        }
    }
}
